Skip to content

6.2 Test

本节介绍 LangChain 代理的测试方法,包括单元测试和集成测试两种策略。


概述

测试代理应用与测试传统软件有本质区别。由于代理行为的不确定性和对外部 LLM 的依赖,我们需要采用专门的测试策略:

┌─────────────────────────────────────────────────────────────┐
│                    Agent 测试策略                            │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌─────────────────┐          ┌─────────────────┐           │
│  │    单元测试      │          │    集成测试      │           │
│  │  (Unit Tests)   │          │ (Integration)   │           │
│  ├─────────────────┤          ├─────────────────┤           │
│  │ - Mock 模型响应  │          │ - 真实 LLM 调用  │           │
│  │ - 内存检查点    │          │ - 轨迹评估       │           │
│  │ - 快速反馈      │          │ - LLM 作为裁判   │           │
│  │ - CI/CD 友好    │          │ - 端到端验证     │           │
│  └─────────────────┘          └─────────────────┘           │
│                                                              │
└─────────────────────────────────────────────────────────────┘
测试类型目的速度成本
单元测试验证逻辑正确性
集成测试验证真实行为

单元测试

Mock 聊天模型

LangChain 提供 GenericFakeChatModel 用于模拟 LLM 响应,无需网络调用:

python
from langchain_core.messages import AIMessage
from langchain_core.language_models import GenericFakeChatModel

# 创建模拟模型
fake_model = GenericFakeChatModel(
    messages=iter([
        AIMessage(content="我来帮你搜索天气信息"),
        AIMessage(content="北京今天晴,25度"),
    ])
)

# 模型按顺序返回预设响应
response1 = fake_model.invoke([{"role": "user", "content": "查天气"}])
print(response1.content)  # "我来帮你搜索天气信息"

response2 = fake_model.invoke([{"role": "user", "content": "结果呢"}])
print(response2.content)  # "北京今天晴,25度"

模拟工具调用

对于带工具的代理,需要模拟工具调用响应:

python
from langchain_core.messages import AIMessage, ToolCall

# 创建带工具调用的响应
tool_response = AIMessage(
    content="",
    tool_calls=[
        ToolCall(
            id="call_123",
            name="weather_search",
            args={"city": "北京"}
        )
    ]
)

fake_model = GenericFakeChatModel(
    messages=iter([
        tool_response,
        AIMessage(content="北京今天晴,温度25度"),
    ])
)

状态持久化测试

使用 InMemorySaver 测试多轮对话的状态保持:

python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, MessagesState, START, END

# 创建带检查点的图
checkpointer = InMemorySaver()

graph = StateGraph(MessagesState)
# ... 添加节点和边 ...
agent = graph.compile(checkpointer=checkpointer)

# 测试多轮对话
config = {"configurable": {"thread_id": "test-thread-1"}}

# 第一轮
result1 = agent.invoke(
    {"messages": [{"role": "user", "content": "记住我叫小明"}]},
    config=config
)

# 第二轮 - 验证状态保持
result2 = agent.invoke(
    {"messages": [{"role": "user", "content": "我叫什么名字?"}]},
    config=config
)

# 断言状态被正确保持
assert "小明" in result2["messages"][-1].content

完整单元测试示例

python
# tests/test_agent.py
import pytest
from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.language_models import GenericFakeChatModel
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, MessagesState, START, END


class TestWeatherAgent:
    """天气代理单元测试"""

    def setup_method(self):
        """每个测试前设置 Mock"""
        self.fake_responses = [
            AIMessage(content="正在查询天气..."),
            AIMessage(content="北京:晴,25°C"),
        ]
        self.fake_model = GenericFakeChatModel(
            messages=iter(self.fake_responses)
        )

    def test_basic_query(self):
        """测试基本查询"""
        response = self.fake_model.invoke([
            HumanMessage(content="查询北京天气")
        ])
        assert response.content == "正在查询天气..."

    def test_multi_turn_conversation(self):
        """测试多轮对话"""
        checkpointer = InMemorySaver()

        # 创建简单图
        def agent_node(state: MessagesState):
            return {"messages": [self.fake_model.invoke(state["messages"])]}

        graph = StateGraph(MessagesState)
        graph.add_node("agent", agent_node)
        graph.add_edge(START, "agent")
        graph.add_edge("agent", END)

        agent = graph.compile(checkpointer=checkpointer)

        config = {"configurable": {"thread_id": "test-1"}}

        # 第一轮
        result1 = agent.invoke(
            {"messages": [HumanMessage(content="你好")]},
            config=config
        )

        # 验证响应
        assert len(result1["messages"]) == 2
        assert result1["messages"][-1].content == "正在查询天气..."

    def test_state_persistence(self):
        """测试状态持久化"""
        checkpointer = InMemorySaver()

        # 构建图并测试状态
        # ... 具体实现 ...
        pass


# 运行测试
# pytest tests/test_agent.py -v

集成测试

集成测试使用真实 LLM,验证代理的实际行为。

安装评估包

bash
pip install agentevals

轨迹匹配评估

agentevals 提供四种轨迹匹配模式:

┌─────────────────────────────────────────────────────────────┐
│                    轨迹匹配模式                              │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  Strict (严格)                                               │
│  ├── 消息顺序必须完全一致                                    │
│  └── 工具调用顺序必须完全一致                                │
│                                                              │
│  Unordered (无序)                                            │
│  ├── 相同的工具调用                                          │
│  └── 顺序可以不同                                            │
│                                                              │
│  Subset (子集)                                               │
│  ├── 代理只调用参考轨迹中的工具                              │
│  └── 不允许额外调用                                          │
│                                                              │
│  Superset (超集)                                             │
│  ├── 代理至少调用参考轨迹中的工具                            │
│  └── 允许额外调用                                            │
│                                                              │
└─────────────────────────────────────────────────────────────┘

轨迹匹配示例

python
from agentevals.trajectory import create_trajectory_match_evaluator

# 创建评估器
evaluator = create_trajectory_match_evaluator(
    trajectory_match_mode="unordered"  # 无序匹配
)

# 定义参考轨迹
reference_trajectory = [
    {"role": "user", "content": "搜索北京天气并计算平均温度"},
    {"role": "assistant", "tool_calls": [
        {"name": "weather_search", "args": {"city": "北京"}}
    ]},
    {"role": "tool", "content": "最高30度,最低20度"},
    {"role": "assistant", "tool_calls": [
        {"name": "calculator", "args": {"expression": "(30+20)/2"}}
    ]},
    {"role": "tool", "content": "25"},
    {"role": "assistant", "content": "北京平均温度25度"},
]

# 实际执行轨迹
actual_trajectory = agent.invoke(
    {"messages": [{"role": "user", "content": "搜索北京天气并计算平均温度"}]}
)

# 评估
result = evaluator(
    outputs=actual_trajectory["messages"],
    reference_outputs=reference_trajectory
)

print(f"匹配分数: {result['score']}")
print(f"详细信息: {result['reasoning']}")

LLM 作为裁判

当轨迹难以精确定义时,使用 LLM 进行定性评估:

python
from agentevals.trajectory import create_trajectory_llm_as_judge

# 创建 LLM 评估器
judge = create_trajectory_llm_as_judge(
    model="gpt-4o",
    rubric="""
    评估代理是否正确完成了用户请求:
    1. 是否理解了用户意图
    2. 是否使用了正确的工具
    3. 最终答案是否准确
    4. 过程是否高效(没有不必要的步骤)

    评分标准:
    - 5分:完美完成
    - 4分:基本完成,有小瑕疵
    - 3分:部分完成
    - 2分:有明显错误
    - 1分:完全失败
    """
)

# 评估
result = judge(
    inputs={"messages": [{"role": "user", "content": "帮我预订明天的机票"}]},
    outputs=actual_trajectory["messages"]
)

print(f"评分: {result['score']}/5")
print(f"评语: {result['reasoning']}")

异步评估

对于大规模测试,使用异步版本:

python
from agentevals.trajectory import create_async_trajectory_llm_as_judge
import asyncio

async def run_async_evaluation():
    judge = create_async_trajectory_llm_as_judge(
        model="gpt-4o",
        rubric="评估代理行为是否正确..."
    )

    # 并行评估多个轨迹
    tasks = [
        judge(inputs=inp, outputs=out)
        for inp, out in test_cases
    ]

    results = await asyncio.gather(*tasks)
    return results

# 运行
results = asyncio.run(run_async_evaluation())

LangSmith 集成

Pytest 标记

使用 @pytest.mark.langsmith 自动记录测试结果:

python
import pytest
from langsmith import testing as ls_testing

@pytest.mark.langsmith
def test_agent_behavior():
    """测试代理行为 - 结果会自动上传到 LangSmith"""

    # 执行代理
    result = agent.invoke({
        "messages": [{"role": "user", "content": "你好"}]
    })

    # 断言
    assert "你好" in result["messages"][-1].content or \
           "您好" in result["messages"][-1].content

    # 返回输出供 LangSmith 记录
    return result

使用 Evaluate 函数

创建数据集并系统化测试:

python
from langsmith import Client

client = Client()

# 创建数据集
dataset = client.create_dataset("agent-test-cases")

# 添加测试用例
client.create_examples(
    dataset_id=dataset.id,
    inputs=[
        {"messages": [{"role": "user", "content": "查询天气"}]},
        {"messages": [{"role": "user", "content": "计算 1+1"}]},
        {"messages": [{"role": "user", "content": "搜索新闻"}]},
    ],
    outputs=[
        {"expected": "天气信息"},
        {"expected": "2"},
        {"expected": "新闻列表"},
    ]
)

# 定义评估函数
def evaluate_agent(inputs):
    return agent.invoke(inputs)

# 运行评估
results = client.evaluate(
    evaluate_agent,
    data=dataset.name,
    evaluators=[
        # 添加评估器
    ]
)

print(f"通过率: {results.summary['pass_rate']}")

HTTP 调用管理

使用 vcrpy 录制回放

减少测试成本和延迟:

python
# conftest.py
import pytest

@pytest.fixture(scope="module")
def vcr_config():
    return {
        "filter_headers": [
            "authorization",
            "x-api-key",
        ],
        "filter_query_parameters": [
            "api_key",
        ],
        "record_mode": "once",  # 只录制一次
    }
python
# test_with_vcr.py
import pytest

@pytest.mark.vcr()
def test_agent_with_vcr():
    """
    首次运行:录制 API 调用到 cassette 文件
    后续运行:回放录制的响应
    """
    result = agent.invoke({
        "messages": [{"role": "user", "content": "测试"}]
    })

    assert result is not None

Cassette 文件结构

tests/
├── cassettes/
│   ├── test_agent_with_vcr.yaml
│   └── test_multi_turn.yaml
├── conftest.py
└── test_agent.py

测试策略矩阵

场景推荐方法说明
本地开发单元测试 + Mock快速反馈,无成本
CI/CD单元测试 + VCR录制一次,重复使用
发布前集成测试真实 LLM 验证
回归测试轨迹匹配确保行为一致
质量评估LLM 裁判定性分析

完整测试套件示例

python
# tests/conftest.py
import pytest
from langchain_core.language_models import GenericFakeChatModel
from langchain_core.messages import AIMessage

@pytest.fixture
def mock_model():
    """提供 Mock 模型"""
    return GenericFakeChatModel(
        messages=iter([
            AIMessage(content="Mock 响应 1"),
            AIMessage(content="Mock 响应 2"),
        ])
    )

@pytest.fixture
def vcr_config():
    """VCR 配置"""
    return {
        "filter_headers": ["authorization"],
        "record_mode": "once",
    }
python
# tests/test_agent.py
import pytest
from my_agent import create_agent

class TestAgentUnit:
    """单元测试"""

    def test_basic_response(self, mock_model):
        agent = create_agent(model=mock_model)
        result = agent.invoke({"messages": [{"role": "user", "content": "hi"}]})
        assert len(result["messages"]) > 0

    def test_tool_selection(self, mock_model):
        # 测试工具选择逻辑
        pass


class TestAgentIntegration:
    """集成测试"""

    @pytest.mark.vcr()
    def test_real_conversation(self):
        agent = create_agent()
        result = agent.invoke({
            "messages": [{"role": "user", "content": "你好"}]
        })
        assert result["messages"][-1].content

    @pytest.mark.langsmith
    def test_with_langsmith_tracking(self):
        agent = create_agent()
        result = agent.invoke({
            "messages": [{"role": "user", "content": "测试"}]
        })
        return result

最佳实践

测试金字塔

                    ┌───────────┐
                    │  E2E 测试  │  ← 少量,验证关键流程
                   ┌┴───────────┴┐
                   │  集成测试    │  ← 适量,验证真实行为
                  ┌┴─────────────┴┐
                  │    单元测试    │  ← 大量,覆盖边界情况
                  └───────────────┘

建议

  1. 优先单元测试:覆盖核心逻辑,快速反馈
  2. 使用 VCR:减少集成测试成本
  3. 定期真实测试:确保与 LLM 的兼容性
  4. 跟踪结果:利用 LangSmith 分析趋势
  5. 测试边界情况:空输入、超长输入、异常输入

上一节6.1 LangSmith Studio

下一节6.3 Agent Chat UI

基于 MIT 许可证发布。内容版权归作者所有。